﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Input;
using Amazon.SimpleDB;
using Amazon.SimpleDB.Model;
using SimpleDbBrowser.ViewModel.Diagnostics;
using SimpleDbBrowser.ViewModel.Properties;
using AmzAttribute = Amazon.SimpleDB.Model.Attribute;

namespace SimpleDbBrowser.ViewModel
{
    /// <summary>
    /// The view model that controls the primary application window and initializes application state.
    /// </summary>
    public sealed class MainViewModel : ViewModelBase
    {
        private const int TraceBufferLength = 10000;
        private readonly ObservableCollection<AmzAttribute> attributes = new ObservableCollection<AmzAttribute>();
        private readonly ObservableCollection<string> dmns = new ObservableCollection<string>();
        private readonly ObservableCollection<Item> items = new ObservableCollection<Item>();
        private readonly StringBuilder traceLog = new StringBuilder(TraceBufferLength);

        private CreateDomainViewModel createVm;
        private CustomSelectViewModel customSelectVm;
        private AmazonSimpleDBClient db;
        private DeleteAttributeViewModel deleteAttrVm;
        private DeleteDomainViewModel deleteVm;
        private PutAttributeViewModel putVm;
        private OptionsViewModel optionsVm;
        private AmzAttribute selectedAttribute;
        private string selectedDomain;
        private Item selectedItem;
        private bool configValid;

        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel()
        {
            this.AssignObservableTraceListener();

            this.DisplayName = "AWS SimpleDB Browser";

            // Load all appropriate data from the simpledb pointed to be the 
            // config file.
            var cfg = new AwsConfiguration(
                Settings.Default.AWSAccessKey,
                Settings.Default.AWSSecretKey,
                Settings.Default.ManualProxyConfig,
                Settings.Default.ProxyHost,
                Settings.Default.ProxyPort);

            this.RefreshSimpleDb(cfg);

            this.InitializeCommands();
        }

        /// <summary>
        /// Re-initializes the SimpleDB that is being connected to, based on current AccessKey and SecretKey settings.
        /// </summary>
        public void RefreshSimpleDb(AwsConfiguration configuration)
        {
            if (this.db != null) this.db.Dispose();

            this.configValid = configuration.IsValid;
            if (!configuration.IsValid)
            {
                Trace.WriteLine("Configure the access keys for your SimpleDB in the Options dialog.");
                this.db = new AmazonSimpleDBClient(string.Empty, string.Empty); // Build a dummy client that won't work.
                return; // Don't bother trying to retrieve domain
            }

            this.db = new AmazonSimpleDBClient(configuration.AccessKey, configuration.SecretKey, configuration.SimpleDbConfig);

            this.DisposeViewModel(() => this.deleteVm, vm => this.deleteVm = vm, "DeleteDomainModel");
            this.DisposeViewModel(() => this.createVm, vm => this.createVm = vm, "CreateDomainModel");
            this.DisposeViewModel(() => this.putVm, vm => this.putVm = vm, "PutAttributeModel");
            this.DisposeViewModel(() => this.deleteAttrVm, vm => this.deleteAttrVm = vm, "DeleteAttributeModel");
            this.DisposeViewModel(() => this.customSelectVm, vm => this.customSelectVm = vm, "CustomSelectModel");

            try
            {
                this.RefreshDomainList();
            }
            catch (AmazonSimpleDBException ex)
            {
                Trace.WriteLine(string.Format("Unable to retrieve domains: {0}", ex.Message));
            }
        }

        /// <summary>
        /// Gets the DisplayOptions command.
        /// </summary>
        public ICommand DisplayOptions { get; private set; }

        /// <summary>
        /// Called when [display options].
        /// </summary>
        /// <param name="param">The param.</param>
        private void OnDisplayOptions(object param)
        {
            if (this.OptionsDialogPresenter != null)
            {
                var result = this.OptionsDialogPresenter.ShowDialog();
            }
        }

        /// <summary>
        /// Determines whether the DisplayOptions command can be executed.
        /// </summary>
        /// <param name="param">The CanExecute parameter.</param>
        /// <returns>
        /// <c>true</c> if the DisplayOptions command can be executed; otherwise, <c>false</c>.
        /// </returns>
        private bool CanDisplayOptions(object param)
        {
            return this.OptionsDialogPresenter != null;
        }

        /// <summary>
        /// Gets or sets the options dialog presenter.
        /// </summary>
        /// <value>The options dialog presenter.</value>
        public IDialogPresenter OptionsDialogPresenter { get; set; }

        /// <summary>
        /// Gets the options viewmodel.
        /// </summary>
        public OptionsViewModel OptionsModel
        {
            get
            {
                if (this.optionsVm == null)
                {
                    this.optionsVm = new OptionsViewModel();
                }

                return this.optionsVm;
            }
        }

        /// <summary>
        /// Disposes a view model and sets it to null if it isn't already null,
        /// and fires the corresponding PropertyChanged event.
        /// </summary>
        /// <typeparam name = "TViewModel">
        /// The type of the view model being disposed.
        /// </typeparam>
        /// <param name = "propGet">
        /// A function that retrieves the view model to be disposed.
        /// </param>
        /// <param name = "propSet">
        /// An action that sets the view model being disposed.
        /// </param>
        /// <param name = "propertyName">
        /// The name of the view model's property, supplied to the PropertyChanged event.
        /// </param>
        private void DisposeViewModel<TViewModel>(Func<TViewModel> propGet,
                                                  Action<TViewModel> propSet,
                                                  string propertyName) where TViewModel : ViewModelBase
        {
            if (propGet() != null)
            {
                propGet().Dispose();
                propSet(null);
            }

            this.OnPropertyChanged(propertyName);
        }

        /// <summary>
        /// Initializes the commands that are exposed by this view model.
        /// </summary>
        private void InitializeCommands()
        {
            this.ExitApplication = new RelayCommand(OnExitApplication);
            this.RefreshDomains = new RelayCommand(this.OnRefreshDomains, this.CanRefreshDomains);
            this.RefreshItems = new RelayCommand(this.OnRefreshItems, this.CanRefreshItems);
            this.RefreshAttributes = new RelayCommand(this.OnRefreshAttributes, this.CanRefreshAttributes);
            this.DisplayOptions = new RelayCommand(this.OnDisplayOptions, this.CanDisplayOptions);
        }

        /// <summary>
        /// Disposes of this view model.
        /// </summary>
        protected override void OnDispose()
        {
            this.db.Dispose();
            if (this.deleteVm != null) this.deleteVm.Dispose();
            if (this.createVm != null) this.createVm.Dispose();
            if (this.putVm != null) this.putVm.Dispose();
            if (this.deleteAttrVm != null) this.deleteAttrVm.Dispose();
            if (this.optionsVm != null) this.optionsVm.Dispose();

            base.OnDispose();
        }

        /// <summary>
        /// Places an ObservableTraceListener into the TraceListener collection.
        /// </summary>
        private void AssignObservableTraceListener()
        {
            var listener = new ObservableTextWriterTraceListener(new StringWriter(this.traceLog));
            listener.TracedMessage += (s, a) =>
            {
                while (this.traceLog.Length > TraceBufferLength)
                {
                    var nextNewline = this.traceLog.ToString().IndexOf(Environment.NewLine);
                    this.traceLog.Remove(0, nextNewline + 2);
                }

                this.OnPropertyChanged("TraceLog");
            };

            Trace.Listeners.Add(listener);
        }

        /// <summary>
        /// Perform actions required when the currently selected domain is changed.
        /// </summary>
        private void OnSelectedDomainChanged()
        {
            this.OnPropertyChanged("SelectedDomain");

            if (this.deleteVm != null) this.deleteVm.SelectedDomain = this.selectedDomain;

            this.RefreshItemList();

            if (this.putVm != null) this.putVm.SelectedDomain = this.selectedDomain;
            if (this.deleteAttrVm != null) this.deleteAttrVm.SelectedDomain = this.selectedDomain;
        }

        /// <summary>
        /// Perform actions required when the currently selected item is changed.
        /// </summary>
        private void OnSelectedItemChanged()
        {
            this.OnPropertyChanged("SelectedItem");

            this.RefreshAttributeList();

            if (this.putVm != null) this.putVm.SelectedItem = this.selectedItem;
            if (this.deleteAttrVm != null) this.deleteAttrVm.SelectedItem = this.selectedItem;
        }

        /// <summary>
        /// Perform actions required when the currently selected attribute is changed.
        /// </summary>
        private void OnSelectedAttributeChanged()
        {
            this.OnPropertyChanged("SelectedAttribute");

            if (this.deleteAttrVm != null) this.deleteAttrVm.SelectedAttribute = this.selectedAttribute;
        }

        /// <summary>
        /// Refreshes the list of domains from the simpledb.
        /// </summary>
        private void RefreshDomainList()
        {
            Debug.WriteLine("Refreshing Domain List...");

            var worker = new BackgroundWorker();
            worker.DoWork += (s, o) => o.Result = LoadDomainNames(this.db);

            worker.RunWorkerCompleted += this.OnRefreshDomainListCompleted;

            worker.RunWorkerAsync();
        }

        /// <summary>
        /// Refreshes the list of items from the simpledb.
        /// </summary>
        private void RefreshItemList()
        {
            Debug.WriteLine("Refreshing Item List...");

            var worker = new BackgroundWorker();
            worker.DoWork += (s, a) => a.Result = LoadItems(this.db, this.selectedDomain);

            worker.RunWorkerCompleted += this.OnRefreshItemsCompleted;

            worker.RunWorkerAsync();
        }

        /// <summary>
        /// Refreshes the list of attributes from the simpledb.
        /// </summary>
        private void RefreshAttributeList()
        {
            Debug.WriteLine("Refreshing Attribute List...");

            var worker = new BackgroundWorker();
            worker.DoWork += (o, a) => a.Result = LoadAttributes(this.db, this.selectedDomain, this.selectedItem);

            worker.RunWorkerCompleted += this.OnLoadAttributesCompleted;

            worker.RunWorkerAsync();
        }

        /// <summary>
        /// Actions that must be performed once the list of domains is refreshed.
        /// </summary>
        /// <param name = "param">
        /// The parameter passed in to the background worker.
        /// </param>
        /// <param name = "args">
        /// The results of the worker that finished running.
        /// </param>
        private void OnRefreshDomainListCompleted(object param, RunWorkerCompletedEventArgs args)
        {
            if (args.Error != null)
            {
                Trace.WriteLine(string.Format("An error occurred while retrieving domain names: {0}", args.Error.Message));
                return;
            }

            var result = args.Result as IEnumerable<string>;
            Debug.Assert(result != null);

            this.dmns.Clear();
            foreach (var dmnName in result)
            {
                this.dmns.Add(dmnName);
            }

            Trace.WriteLine("Domain list Refreshed");

            CommandManager.InvalidateRequerySuggested();
        }

        /// <summary>
        /// Actions that must be performed once the list of items is refreshed.
        /// </summary>
        /// <param name = "param">
        /// The parameter passed in to the background worker.
        /// </param>
        /// <param name = "args">
        /// The results of the worker that finished running.
        /// </param>
        private void OnRefreshItemsCompleted(object param, RunWorkerCompletedEventArgs args)
        {
            this.items.Clear();

            if (args.Error != null)
            {
                Trace.WriteLine(string.Format("Unable to refresh item list: {0}", args.Error.Message));
                return;
            }

            foreach (var item in (IEnumerable<Item>)args.Result)
            {
                this.items.Add(item);
            }

            Trace.WriteLine("Items Refreshed");

            CommandManager.InvalidateRequerySuggested();
        }

        /// <summary>
        /// Actions that must be performed once the list of attributes is refreshed.
        /// </summary>
        /// <param name = "param">
        /// The parameter passed in to the background worker.
        /// </param>
        /// <param name = "args">
        /// The results of the worker that finished running.
        /// </param>
        private void OnLoadAttributesCompleted(object param, RunWorkerCompletedEventArgs args)
        {
            this.attributes.Clear();

            if (args.Error != null)
            {
                Trace.WriteLine(string.Format("Unable to refresh attributes: {0}", args.Error.Message));
                return;
            }

            var res = (IEnumerable<AmzAttribute>)args.Result;

            foreach (var attr in res)
            {
                this.attributes.Add(attr);
            }

            Trace.WriteLine("Attributes Refreshed");

            CommandManager.InvalidateRequerySuggested();
        }

        /// <summary>
        /// Synchronously loads domain names from the configured SimpleDB.
        /// </summary>
        /// <param name = "db">
        /// The simpledb that domain names are being listed from.
        /// </param>
        /// <returns>
        /// An enumeration of domain names that exist in the supplied SimpleDB.
        /// </returns>
        private static IEnumerable<string> LoadDomainNames(AmazonSimpleDB db)
        {
            var req = new ListDomainsRequest();
            var resp = db.ListDomains(req);

            return resp.ListDomainsResult.DomainName;
        }

        /// <summary>
        /// Synchronously loads items from the configured SimpleDB.
        /// </summary>
        /// <param name = "db">
        /// The simpledb that items are being pulled from.
        /// </param>
        /// <param name = "domainName">
        /// The name of the domain that items are being loaded from.
        /// </param>
        /// <returns>
        /// An enumeration of items from the SimpleDb.
        /// </returns>
        private static IEnumerable<Item> LoadItems(AmazonSimpleDB db, string domainName)
        {
            if (string.IsNullOrWhiteSpace(domainName)) return new Item[] { };

            var selectExpression = string.Format("select * from `{0}`", domainName);
            var req = new SelectRequest().WithSelectExpression(selectExpression);

            Debug.WriteLine("Submitting Select request: \"" + req.SelectExpression + "\"");
            var resp = db.Select(req);

            return resp.SelectResult.Item;
        }

        /// <summary>
        /// Synchronously loads attributes from the configured SimpleDB.
        /// </summary>
        /// <param name = "db">
        /// The simpledb that attributes are being pulled from.
        /// </param>
        /// <param name = "domainName">
        /// The name of the domain that attributes are being pulled from.
        /// </param>
        /// <param name = "item">
        /// The name of the item that attributes are being pulled from.
        /// </param>
        /// <returns>
        /// An enumeration of attributes from the simpledb.
        /// </returns>
        private static IEnumerable<AmzAttribute> LoadAttributes(AmazonSimpleDB db, string domainName, Item item)
        {
            if (string.IsNullOrWhiteSpace(domainName) || item == null) return new AmzAttribute[] { };

            var req = new GetAttributesRequest().WithDomainName(domainName).WithItemName(item.Name);
            var resp = db.GetAttributes(req);

            return resp.GetAttributesResult.Attribute;
        }

        /// <summary>
        /// Gets or sets the currently selected domain in the UI.
        /// </summary>
        [Bindable(true)]
        public string SelectedDomain
        {
            get { return this.selectedDomain; }

            set
            {
                if (this.selectedDomain == value)
                    return;

                this.selectedDomain = value;
                this.OnSelectedDomainChanged();

                Debug.WriteLine(string.Format("SelectedDomain set to {0}.", this.selectedDomain ?? "<null>"));
            }
        }

        /// <summary>
        /// Gets or sets the currently selected item in the UI.
        /// </summary>
        [Bindable(true)]
        public Item SelectedItem
        {
            get { return this.selectedItem; }

            set
            {
                if (this.selectedItem == value)
                    return;

                this.selectedItem = value;
                this.OnSelectedItemChanged();

                Debug.WriteLine(string.Format("SelectedItem set to {0}.",
                                              this.selectedItem != null ? this.selectedItem.Name : "<null>"));
            }
        }

        /// <summary>
        /// Gets or sets the currently selected attribute in the UI.
        /// </summary>
        [Bindable(true)]
        public AmzAttribute SelectedAttribute
        {
            get { return this.selectedAttribute; }

            set
            {
                if (this.selectedAttribute == value)
                    return;

                this.selectedAttribute = value;
                this.OnSelectedAttributeChanged();

                Debug.WriteLine(string.Format("SelectedAttribute set to {0}.",
                                              this.selectedAttribute != null ? this.selectedAttribute.Name : "<null>"));
            }
        }

        /// <summary>
        /// Gets the current trace log.
        /// </summary>
        [Bindable(true)]
        public string TraceLog
        {
            get { return this.traceLog.ToString(); }
        }

        /// <summary>
        /// Gets the list of domains in this simple db.
        /// </summary>
        [Bindable(true)]
        public ReadOnlyObservableCollection<string> Domains
        {
            get { return new ReadOnlyObservableCollection<string>(this.dmns); }
        }

        /// <summary>
        /// Gets the list of items in the selected domain.
        /// </summary>
        [Bindable(true)]
        public ReadOnlyObservableCollection<Item> Items
        {
            get { return new ReadOnlyObservableCollection<Item>(this.items); }
        }

        /// <summary>
        /// Gets a list of attributes on the selected item.
        /// </summary>
        [Bindable(true)]
        public ReadOnlyObservableCollection<AmzAttribute> Attributes
        {
            get { return new ReadOnlyObservableCollection<AmzAttribute>(this.attributes); }
        }

        /// <summary>
        /// Gets the view model for creating a new domain.
        /// </summary>
        [Bindable(true)]
        public CreateDomainViewModel CreateDomainModel
        {
            get
            {
                if (this.createVm == null)
                {
                    this.createVm = new CreateDomainViewModel(this.db);
                    this.createVm.DomainCreated += (s, o) => this.RefreshDomainList();
                }

                return this.createVm;
            }
        }

        /// <summary>
        /// Gets the view model for deleting an existing domain.
        /// </summary>
        [Bindable(true)]
        public DeleteDomainViewModel DeleteDomainModel
        {
            get
            {
                if (this.deleteVm == null)
                {
                    this.deleteVm = new DeleteDomainViewModel(this.db);
                    this.deleteVm.DomainDeleted += (s, o) => this.RefreshDomainList();
                }

                return this.deleteVm;
            }
        }

        /// <summary>
        /// Gets the view model for adding an attribute to the domain.
        /// </summary>
        [Bindable(true)]
        public PutAttributeViewModel PutAttributeModel
        {
            get
            {
                if (this.putVm == null)
                {
                    this.putVm = new PutAttributeViewModel(this.db);
                    this.putVm.AttributePut += (s, o) =>
                    {
                        // If this addition will result in a new item, refresh it all, otherwise just refresh the attribute list
                        if (this.Items.Where(i => i.Name == this.putVm.ItemName).Count() == 0)
                            this.RefreshItemList();
                        else
                            this.RefreshAttributeList();
                    };
                }

                return this.putVm;
            }
        }

        /// <summary>
        /// Gets the view model for deleting an attribute from the domain.
        /// </summary>
        [Bindable(true)]
        public DeleteAttributeViewModel DeleteAttributeModel
        {
            get
            {
                if (this.deleteAttrVm == null)
                {
                    this.deleteAttrVm = new DeleteAttributeViewModel(this.db);
                    this.deleteAttrVm.AttributeDeleted += (s, o) =>
                    {
                        // If this deletion will kill the owning item, refresh it all, otherwise just refresh the attrs
                        if (this.Items.Where(i => i.Name == this.deleteAttrVm.SelectedItem.Name).First().Attribute.Count() == 1)
                            this.RefreshItemList();
                        else
                            this.RefreshAttributeList();
                    };
                }

                return this.deleteAttrVm;
            }
        }

        /// <summary>
        /// Gets the view model for performing a custom query agains the simpledb.
        /// </summary>
        [Bindable(true)]
        public CustomSelectViewModel CustomSelectModel
        {
            get { return this.customSelectVm ?? (this.customSelectVm = new CustomSelectViewModel(this.db)); }
        }

        /// <summary>
        /// Gets the command that exits the application.
        /// </summary>
        [Bindable(true)]
        public ICommand ExitApplication { get; private set; }

        /// <summary>
        /// Executes the ExitApplication command.
        /// </summary>
        /// <param name = "param">
        /// This is not used.
        /// </param>
        private static void OnExitApplication(object param)
        {
            Environment.Exit(0);
        }

        /// <summary>
        /// Gets the command that refreshes the displayed domains with current values from the simple db.
        /// </summary>
        [Bindable(true)]
        public ICommand RefreshDomains { get; private set; }

        /// <summary>
        /// Executes the RefreshDomains command.
        /// </summary>
        /// <param name = "param">
        /// This is not used.
        /// </param>
        private void OnRefreshDomains(object param)
        {
            this.RefreshDomainList();
        }

        /// <summary>
        /// Determines whether the RefreshDomains command can be executed.
        /// </summary>
        /// <param name="param">
        /// The CanExecute param.  This is not used.
        /// </param>
        /// <returns>
        /// True if the command can be executed, otherwise false.
        /// </returns>
        private bool CanRefreshDomains(object param)
        {
            //return true;
            return this.configValid;
        }

        /// <summary>
        /// Gets the command that refreshes the displayed items with current values from the simple db.
        /// </summary>
        [Bindable(true)]
        public ICommand RefreshItems { get; private set; }

        /// <summary>
        /// Executes the RefreshItems command.
        /// </summary>
        /// <param name = "param">
        /// This is not used.
        /// </param>
        private void OnRefreshItems(object param)
        {
            this.RefreshItemList();
        }

        /// <summary>
        /// Determines whether the RefreshItems command can currently be executed.
        /// </summary>
        /// <param name = "param">
        /// This is not used.
        /// </param>
        /// <returns>
        /// true if the command can be executed, otherwise false.
        /// </returns>
        private bool CanRefreshItems(object param)
        {
            return !string.IsNullOrWhiteSpace(this.selectedDomain);
        }

        /// <summary>
        /// Gets the command that refreshes the displayed attributes with current values from the simple db.
        /// </summary>
        [Bindable(true)]
        public ICommand RefreshAttributes { get; private set; }

        /// <summary>
        /// Executes the RefreshAttributes command.
        /// </summary>
        /// <param name = "param">
        /// This is not used.
        /// </param>
        private void OnRefreshAttributes(object param)
        {
            this.RefreshAttributeList();
        }

        /// <summary>
        /// Determines whether the RefreshAttributes command can currently be executed.
        /// </summary>
        /// <param name = "param">
        /// This is not used.
        /// </param>
        /// <returns>
        /// True if the command can be executed, otherwise false.
        /// </returns>
        private bool CanRefreshAttributes(object param)
        {
            return this.selectedItem != null;
        }
    }
}